const React = require(`react`)
const path = require(`path`)
const {
  renderToStaticMarkup,
  renderToPipeableStream,
} = require(`react-dom/server`)
const { ServerLocation, Router, isRedirect } = require(`@gatsbyjs/reach-router`)
const merge = require(`deepmerge`)
const { StaticQueryContext } = require(`gatsby`)
const fs = require(`fs`)
const { WritableAsPromise } = require(`./server-utils/writable-as-promise`)

const { RouteAnnouncerProps } = require(`./route-announcer-props`)
const { apiRunner, apiRunnerAsync } = require(`./api-runner-ssr`)
const asyncRequires = require(`$virtual/async-requires`)
const { version: gatsbyVersion } = require(`gatsby/package.json`)
const { grabMatchParams } = require(`./find-path`)
const { headHandlerForSSR } = require(`./head/head-export-handler-for-ssr`)
const {
  SlicesResultsContext,
  SlicesContext,
  SlicesMapContext,
  SlicesPropsContext,
} = require(`./slice/context`)
const { ServerSliceRenderer } = require(`./slice/server-slice-renderer`)

// we want to force posix-style joins, so Windows doesn't produce backslashes for urls
const { join } = path.posix

const testRequireError = (moduleName, err) => {
  const regex = new RegExp(`Error: Cannot find module\\s.${moduleName}`)
  const firstLine = err.toString().split(`\n`)[0]
  return regex.test(firstLine)
}

let Html
try {
  Html = require(`../src/html`)
} catch (err) {
  if (testRequireError(`../src/html`, err)) {
    Html = require(`./default-html`)
  } else {
    throw err
  }
}

Html = Html && Html.__esModule ? Html.default : Html

const getPageDataPath = path => {
  const fixedPagePath = path === `/` ? `index` : path
  return join(`page-data`, fixedPagePath, `page-data.json`)
}

const createElement = React.createElement

export const sanitizeComponents = components => {
  const componentsArray = [].concat(components).flat(Infinity).filter(Boolean)

  return componentsArray.map(component => {
    // Ensure manifest is always loaded from content server
    // And not asset server when an assetPrefix is used
    if (__ASSET_PREFIX__ && component.props.rel === `manifest`) {
      return React.cloneElement(component, {
        href: component.props.href.replace(__ASSET_PREFIX__, ``),
      })
    }
    return component
  })
}

function deepMerge(a, b) {
  const combineMerge = (target, source, options) => {
    const destination = target.slice()

    source.forEach((item, index) => {
      if (typeof destination[index] === `undefined`) {
        destination[index] = options.cloneUnlessOtherwiseSpecified(
          item,
          options
        )
      } else if (options.isMergeableObject(item)) {
        destination[index] = merge(target[index], item, options)
      } else if (target.indexOf(item) === -1) {
        destination.push(item)
      }
    })
    return destination
  }

  return merge(a, b, { arrayMerge: combineMerge })
}

/**
Reorder headComponents so meta tags are always at the top and aren't missed by crawlers by being pushed down by large inline styles, etc.
@see https://github.com/gatsbyjs/gatsby/issues/22206
*/
export const reorderHeadComponents = headComponents => {
  const sorted = headComponents.sort((a, b) => {
    if (a.type && a.type === `meta` && !(b.type && b.type === `meta`)) {
      return -1
    }
    return 0
  })

  return sorted
}

const DEFAULT_CONTEXT = {
  // whether or not we're building the site now
  // usage in determining original build or engines
  isDuringBuild: false,
}

export default async function staticPage({
  pagePath,
  pageData,
  staticQueryContext,
  styles,
  scripts,
  reversedStyles,
  reversedScripts,
  inlinePageData = false,
  context = {},
  webpackCompilationHash,
  sliceData,
}) {
  const renderContext = Object.assign(DEFAULT_CONTEXT, context)

  // for this to work we need this function to be sync or at least ensure there is single execution of it at a time
  global.unsafeBuiltinUsage = []

  try {
    let bodyHtml = ``
    let headComponents = [
      <meta
        name="generator"
        content={`Gatsby ${gatsbyVersion}`}
        key={`generator-${gatsbyVersion}`}
      />,
    ]
    let htmlAttributes = {}
    let bodyAttributes = {}
    let preBodyComponents = []
    let postBodyComponents = []
    let bodyProps = {}

    function loadPageDataSync(_pagePath) {
      if (_pagePath === pagePath) {
        // no need to use fs if we are asking for pageData of current page
        return pageData
      }

      const pageDataPath = getPageDataPath(_pagePath)
      const pageDataFile = join(process.cwd(), `public`, pageDataPath)
      try {
        // deprecation notice
        const myErrorHolder = {
          name: `Usage of loadPageDataSync for page other than currently generated page disables incremental html generation in future builds`,
        }
        Error.captureStackTrace(myErrorHolder, loadPageDataSync)
        global.unsafeBuiltinUsage.push(myErrorHolder.stack)
        const pageDataJson = fs.readFileSync(pageDataFile)
        return JSON.parse(pageDataJson)
      } catch (error) {
        // not an error if file is not found. There's just no page data
        return null
      }
    }

    const replaceBodyHTMLString = body => {
      bodyHtml = body
    }

    const setHeadComponents = components => {
      headComponents = headComponents.concat(sanitizeComponents(components))
    }

    const setHtmlAttributes = attributes => {
      // TODO - we should remove deep merges
      htmlAttributes = deepMerge(htmlAttributes, attributes)
    }

    const setBodyAttributes = attributes => {
      // TODO - we should remove deep merges
      bodyAttributes = deepMerge(bodyAttributes, attributes)
    }

    const setPreBodyComponents = components => {
      preBodyComponents = preBodyComponents.concat(
        sanitizeComponents(components)
      )
    }

    const setPostBodyComponents = components => {
      postBodyComponents = postBodyComponents.concat(
        sanitizeComponents(components)
      )
    }

    const setBodyProps = props => {
      // TODO - we should remove deep merges
      bodyProps = deepMerge({}, bodyProps, props)
    }

    const getHeadComponents = () => headComponents

    const replaceHeadComponents = components => {
      headComponents = sanitizeComponents(components)
    }

    const getPreBodyComponents = () => preBodyComponents

    const replacePreBodyComponents = components => {
      preBodyComponents = sanitizeComponents(components)
    }

    const getPostBodyComponents = () => postBodyComponents

    const replacePostBodyComponents = components => {
      postBodyComponents = sanitizeComponents(components)
    }

    const { componentChunkName, slicesMap } = pageData
    const pageComponent = await asyncRequires.components[componentChunkName]()

    class RouteHandler extends React.Component {
      render() {
        const props = {
          ...this.props,
          ...pageData.result,
          params: {
            ...grabMatchParams(this.props.location.pathname),
            ...(pageData.result?.pageContext?.__params || {}),
          },
        }

        const pageElement = createElement(pageComponent.default, props)

        const wrappedPage = apiRunner(
          `wrapPageElement`,
          { element: pageElement, props },
          pageElement,
          ({ result }) => {
            return { element: result, props }
          }
        ).pop()

        return wrappedPage
      }
    }

    const routerElement = (
      <ServerLocation url={`${__BASE_PATH__}${pagePath}`}>
        <Router id="gatsby-focus-wrapper" baseuri={__BASE_PATH__}>
          <RouteHandler path="/*" />
        </Router>
        <div {...RouteAnnouncerProps} />
      </ServerLocation>
    )

    const sliceProps = {}

    let body = apiRunner(
      `wrapRootElement`,
      { element: routerElement, pathname: pagePath },
      routerElement,
      ({ result }) => {
        return { element: result, pathname: pagePath }
      }
    ).pop()

    const slicesContext = {
      // if we're in build now, we know we're on the server
      // otherwise we're in an engine
      renderEnvironment: renderContext.isDuringBuild ? `server` : `engines`,
    }
    if (process.env.GATSBY_SLICES) {
      // if we're running in an engine, we need to manually wrap body with
      // the results context to pass the map of slice name to component/data/context
      if (slicesContext.renderEnvironment === `engines`) {
        // this is the same name used in the browser
        // since this immitates behavior
        const slicesDb = new Map()

        for (const sliceName of Object.values(slicesMap)) {
          const slice = sliceData[sliceName]
          const { default: SliceComponent } = await getPageChunk(slice)

          const sliceObject = {
            component: SliceComponent,
            sliceContext: slice.result.sliceContext,
            data: slice.result.data,
          }

          slicesDb.set(sliceName, sliceObject)
        }

        body = (
          <SlicesResultsContext.Provider value={slicesDb}>
            {body}
          </SlicesResultsContext.Provider>
        )
      }

      body = (
        <SlicesContext.Provider value={slicesContext}>
          <SlicesPropsContext.Provider value={sliceProps}>
            <SlicesMapContext.Provider value={slicesMap}>
              {body}
            </SlicesMapContext.Provider>
          </SlicesPropsContext.Provider>
        </SlicesContext.Provider>
      )
    }

    const bodyComponent = (
      <StaticQueryContext.Provider value={staticQueryContext}>
        {body}
      </StaticQueryContext.Provider>
    )

    // Let the site or plugin render the page component.
    await apiRunnerAsync(`replaceRenderer`, {
      bodyComponent,
      replaceBodyHTMLString,
      setHeadComponents,
      setHtmlAttributes,
      setBodyAttributes,
      setPreBodyComponents,
      setPostBodyComponents,
      setBodyProps,
      pathname: pagePath,
      pathPrefix: __PATH_PREFIX__,
    })

    // If no one stepped up, we'll handle it.
    if (!bodyHtml) {
      try {
        const writableStream = new WritableAsPromise()
        const { pipe } = renderToPipeableStream(bodyComponent, {
          onAllReady() {
            pipe(writableStream)
          },
          onError(error) {
            writableStream.destroy(error)
          },
        })

        bodyHtml = await writableStream
      } catch (e) {
        // ignore @reach/router redirect errors
        if (!isRedirect(e)) throw e
      }
    }

    apiRunner(`onRenderBody`, {
      setHeadComponents,
      setHtmlAttributes,
      setBodyAttributes,
      setPreBodyComponents,
      setPostBodyComponents,
      setBodyProps,
      pathname: pagePath,
      loadPageDataSync,
      bodyHtml,
      scripts,
      styles,
      pathPrefix: __PATH_PREFIX__,
    })

    // we want to run Head after onRenderBody, so Html and Body attributes
    // from Head wins over global ones from onRenderBody
    headHandlerForSSR({
      pageComponent,
      setHeadComponents,
      setHtmlAttributes,
      setBodyAttributes,
      staticQueryContext,
      pageData,
      pagePath,
    })

    reversedScripts.forEach(script => {
      // Add preload/prefetch <link>s magic comments
      if (script.shouldGenerateLink) {
        headComponents.push(
          <link
            as="script"
            rel={script.rel}
            key={script.name}
            href={`${__PATH_PREFIX__}/${script.name}`}
          />
        )
      }
    })

    reversedStyles.forEach(style => {
      // Add <link>s for styles that should be prefetched
      // otherwise, inline as a <style> tag

      if (style.rel === `prefetch`) {
        headComponents.push(
          <link
            as="style"
            rel={style.rel}
            key={style.name}
            href={`${__PATH_PREFIX__}/${style.name}`}
          />
        )
      } else {
        headComponents.unshift(
          <style
            data-href={`${__PATH_PREFIX__}/${style.name}`}
            data-identity={`gatsby-global-css`}
            dangerouslySetInnerHTML={{
              __html: style.content,
            }}
          />
        )
      }
    })

    // Add page metadata for the current page
    const windowPageData = `/*<![CDATA[*/window.pagePath=${JSON.stringify(
      pagePath
    )};${
      process.env.GATSBY_SLICES
        ? ``
        : `window.___webpackCompilationHash="${webpackCompilationHash}";`
    }${
      inlinePageData ? `window.pageData=${JSON.stringify(pageData)};` : ``
    }/*]]>*/`

    postBodyComponents.push(
      <script
        key={`script-loader`}
        id={`gatsby-script-loader`}
        dangerouslySetInnerHTML={{
          __html: windowPageData,
        }}
      />
    )

    if (process.env.GATSBY_SLICES) {
      postBodyComponents.push(
        createElement(ServerSliceRenderer, {
          sliceId: `_gatsby-scripts`,
        })
      )
    } else {
      const chunkMapping = require(`../public/chunk-map.json`)
      // restore the old behavior
      // Add chunk mapping metadata
      const scriptChunkMapping = `/*<![CDATA[*/window.___chunkMapping=${JSON.stringify(
        chunkMapping
      )};/*]]>*/`

      postBodyComponents.push(
        <script
          key={`chunk-mapping`}
          id={`gatsby-chunk-mapping`}
          dangerouslySetInnerHTML={{
            __html: scriptChunkMapping,
          }}
        />
      )

      let bodyScripts = []
      if (chunkMapping[`polyfill`]) {
        chunkMapping[`polyfill`].forEach(script => {
          const scriptPath = `${__PATH_PREFIX__}${script}`
          bodyScripts.push(
            <script key={scriptPath} src={scriptPath} noModule={true} />
          )
        })
      }

      // Filter out prefetched bundles as adding them as a script tag
      // would force high priority fetching.
      bodyScripts = bodyScripts.concat(
        scripts
          .filter(s => s.rel !== `prefetch`)
          .map(s => {
            const scriptPath = `${__PATH_PREFIX__}/${JSON.stringify(
              s.name
            ).slice(1, -1)}`
            return <script key={scriptPath} src={scriptPath} async />
          })
      )

      postBodyComponents.push(...bodyScripts)
    }

    headComponents = reorderHeadComponents(headComponents)

    apiRunner(`onPreRenderHTML`, {
      getHeadComponents,
      replaceHeadComponents,
      getPreBodyComponents,
      replacePreBodyComponents,
      getPostBodyComponents,
      replacePostBodyComponents,
      pathname: pagePath,
      pathPrefix: __PATH_PREFIX__,
    })

    let htmlElement = (
      <Html
        {...bodyProps}
        headComponents={headComponents}
        htmlAttributes={htmlAttributes}
        bodyAttributes={bodyAttributes}
        preBodyComponents={preBodyComponents}
        postBodyComponents={postBodyComponents}
        body={bodyHtml}
        path={pagePath}
      />
    )

    if (process.env.GATSBY_SLICES) {
      htmlElement = (
        <SlicesContext.Provider value={slicesContext}>
          {htmlElement}
        </SlicesContext.Provider>
      )
    }

    const html = `<!DOCTYPE html>${renderToStaticMarkup(htmlElement)}`

    return {
      html,
      unsafeBuiltinsUsage: global.unsafeBuiltinUsage,
      sliceData: sliceProps,
    }
  } catch (e) {
    e.unsafeBuiltinsUsage = global.unsafeBuiltinUsage
    throw e
  }
}

export function getPageChunk({ componentChunkName }) {
  return asyncRequires.components[componentChunkName]()
}

export { renderToPipeableStream } from "react-server-dom-webpack/writer.node.server"
export { StaticQueryContext, React }

export async function renderSlice({ slice, staticQueryContext, props = {} }) {
  const { default: SliceComponent } = await getPageChunk(slice)

  const slicesContext = {
    // we are not yet supporting using <Slice /> placeholders within slice components
    // setting this renderEnvironemnt to throw meaningful error on `<Slice />` usage
    // `slices` renderEnvironment should be removed once we support nested `<Slice />` placeholders
    renderEnvironment: `slices`,
    sliceRoot: slice,
  }

  const sliceElement = (
    <SliceComponent sliceContext={slice.context} {...props} />
  )

  const sliceWrappedWithWrapRootElement = apiRunner(
    `wrapRootElement`,
    { element: sliceElement },
    sliceElement,
    ({ result }) => {
      return { element: result }
    }
  ).pop()

  const sliceWrappedWithWrapRootElementAndContexts = (
    <SlicesContext.Provider value={slicesContext}>
      <StaticQueryContext.Provider value={staticQueryContext}>
        {sliceWrappedWithWrapRootElement}
      </StaticQueryContext.Provider>
    </SlicesContext.Provider>
  )

  const writableStream = new WritableAsPromise()
  const { pipe } = renderToPipeableStream(
    sliceWrappedWithWrapRootElementAndContexts,
    {
      onAllReady() {
        pipe(writableStream)
      },
      onError(error) {
        writableStream.destroy(error)
      },
    }
  )

  return await writableStream
}
